OneShell

I fight for a brighter tomorrow

0%

[pwnable.kr] input

1
2
3
Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char* argv[], char* envp[]){
printf("Welcome to pwnable.kr\n");
printf("Let's see if you know how to give input to program\n");
printf("Just give me correct inputs then you will get the flag :)\n");

// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");

// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");

// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");

// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

// here's your flag
system("/bin/cat flag");
return 0;
}

逐个整理要求:

stage1:

1
2
3
4
5
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");
  • argc=100:需要额外的99个参数
  • argv[‘A’] = argv[65] = \x00
  • argv[‘B’] = argv[66] = \x20\x0a\x0d

可以使用C代码写一个额外的程序来执行程序input,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main() {
int i;
char *args[101] = {};

for (i = 0; i < 101; ++i)
args[i] = "A";
args['A'] = "\x00";
args['B'] = "\x20\x0a\x0d";
args[100] = NULL;
execve("./input", args, NULL);
}

一定要对字符串指针数组args进行初始化操作,如上的for循环,指向了静态数组”A”,execve执行的时候,识别不到args的大小。

stage2:

1
2
3
4
5
6
7
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");
  • 从标准输入STDIN,也就是fd=0读取4个字节到buf,内容是\x00\xoa\x00\xff
  • 从标准错误STDERR,也就是fd=2读取4个字节到buf,内容是\x00\xoa\x02\xff

可以考虑使用管道的方式,来劫持程序的STDIN和STDERR,
首先定义两个管道pipe_stdin[2]、pipe_stderr[2],定义成大小为2的int数组,是因为一个负责写入、一个负责读取。

1
2
3
pid_t child_pid;
int pipe_stdin[2];
int pipe_stderr[2];

使用pipe()创建管道

1
2
3
4
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
perror("error creating pipes\n");
exit(1);
}

随后使用fork()创建子进程,这样可以在父进程中通过管道控制子进程的STDIN和STDERR:

1
2
3
4
if ((child_pid = fork()) < 0) {
perror("error forking child\n");
exit(1);
}

然后根据父进程和子进程的管道数据传输关系,关闭管道相应的文件描述符。例如我们需要父进程向子进程的标准输入STDIN发送数据,那么父进程需要关闭读取端pipe_stdin[0]、子进程需要关闭写入段pipe_stdin[1]。使用dup2复制管道描述符到标准输入和标准错误,最终如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (child_pid == 0) {
close(pipe_stdin[0]);
close(pipe_stderr[0]);

write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
write(pipe_stderr[1], "\x00\xoa\x02\xff", 4);

return 0;
} else {
close(pipe_stdin[1]);
close(pipe_stderr[1]);

dup2(pipe_stdin[0], 0);
dup2(pipe_stderr[0], 2);

close(pipe_stdin[0]);
close(pipe_stderr[0]);
execve("./input", args, NULL);
}

stage3:

1
2
3
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");

需要将环境变量deadbeef设置成值cafebabe,很简单,直接使用函数setenv即可。

1
2
3
4
setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);
extern char** environ;
......
execve("./input", args, environ);

stage4:

1
2
3
4
5
6
7
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

这个阶段是为了打开一个文件\x0a,读取其中的内容,内容需要为\x00\x00\x00\x00

我们的代码中可以创建该文件,然后调用程序input。在pwnable的上机环境中,可以在文件夹/tmp创建相应的文件。

1
2
3
FILE* fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

stage5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

该代码创建了一个IPv4的TCP协议的socket,绑定在argv[‘C’]指定的端口上,并等待最多一个客户端连接;然后使用accept接受来自客户端的请求,并使用recv读取4个字节,将输入存储到buf中;最后校验buf是否等于\xde\xad\xbe\xef

那么就可以在父进程中创建一个客户端去连接子进程中的socket,并输入数据。在父进程中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
args['C'] = "6666";

sleep(5); // 等待input创建好
int sd, cd;
struct sockaddr_in saddr;
sd = socket(AF_INET, SOCK_STREAM, 0);

if (sd == -1) {
printf("error creating socket\n");
return 1;
}

saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if (connect(sd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
printf("error connecting\n");
return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
close(sd);

exp

完整exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main() {
int i;
char *args[101] = {};
pid_t child_pid;
int pipe_stdin[2];
int pipe_stderr[2];

extern char** environ;

setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);

for (i = 0; i < 101; ++i)
args[i] = "A";

args['A'] = "\x00";
args['B'] = "\x20\x0a\x0d";
args['C'] = "6666";
args[100] = NULL;
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
perror("error creating pipes\n");
exit(1);
}

if ((child_pid = fork()) < 0) {
perror("error forking child\n");
exit(1);
}

if (child_pid == 0) {
close(pipe_stdin[0]);
close(pipe_stderr[0]);

write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);

sleep(5); // 等待input创建好
int sd, cd;
struct sockaddr_in saddr;
sd = socket(AF_INET, SOCK_STREAM, 0);

if (sd == -1) {
printf("error creating socket\n");
return 1;
}

saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if (connect(sd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
printf("error connecting\n");
return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
close(sd);
return 0;

} else {
close(pipe_stdin[1]);
close(pipe_stderr[1]);

dup2(pipe_stdin[0], 0);
dup2(pipe_stderr[0], 2);

close(pipe_stdin[0]);
close(pipe_stderr[0]);

FILE* fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

execve("/home/input2/input", args, environ);
}
}

编译直接运行,并不会输出flag,因为在当前目录下没有flag文件,因此我们需要创建一个链接到flag文件。

1
2
3
4
5
6
7
8
9
input2@pwnable:/tmp/input_own$ ./input
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!

如下:

1
2
3
4
5
6
7
8
9
10
11
input2@pwnable:/tmp/input_own$ ln -sf /home/input2/flag flag
input2@pwnable:/tmp/input_own$ ./input
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
Mommy! I learned how to pass various input in Linux :)

知识点小结

  • args参数、envs环境变量,两个变量类型都是字符串指针数组,其中的每一个元素都指向一个字符串,而且每一个元素的值都不为空(除了最后一个元素)
  • socket、pipe编程在网络编程中会经常被使用到;pipe、dup2在AFL的源码中也有被使用到,用于将测试数据喂到fork出来的目标程序子进程的标准输入中。